/*
 * Copyright 2002-2006 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.remoting.jsonrpc;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Writer;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.text.ParseException;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.WordUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jabsorb.JSONSerializer;
import org.jabsorb.serializer.MarshallException;
import org.jabsorb.serializer.SerializerState;
import org.jabsorb.serializer.UnmarshallException;
import org.json.JSONException;
import org.json.JSONObject;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.remoting.support.RemoteExporter;
import org.springframework.remoting.support.RemoteInvocation;
import org.springframework.web.HttpRequestHandler;

public class JSONRPCServiceExporter extends RemoteExporter implements HttpRequestHandler, InitializingBean,
    BeanNameAware {

  private JSONSerializer serializer;

  private String beanName;

  private AJAXHandler ajaxHandler;

  private static final Log logger = LogFactory.getLog(JSONRPCServiceExporter.class);

  public void setSerializer(JSONSerializer serializer) {
    this.serializer = serializer;
  }

  public void afterPropertiesSet() {
    /*
     * try {
     * 
     * serializer.registerSerializer(new BeanSerializer());
     * serializer.registerSerializer(new ArraySerializer());
     * serializer.registerSerializer(new DictionarySerializer());
     * serializer.registerSerializer(new MapSerializer());
     * serializer.registerSerializer(new SetSerializer());
     * serializer.registerSerializer(new ListSerializer());
     * serializer.registerSerializer(new DateSerializer());
     * serializer.registerSerializer(new StringSerializer());
     * serializer.registerSerializer(new NumberSerializer());
     * serializer.registerSerializer(new BooleanSerializer());
     * serializer.registerSerializer(new PrimitiveSerializer());
     * serializer.registerSerializer(new ClassSerializer());
     * serializer.registerSerializer(new ObjectSerializer()); } catch (Exception
     * e) { //TODO what to do with this exception? e.printStackTrace(); }
     */
  }

  public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException,
      IOException {
    if (request.getMethod().equalsIgnoreCase("GET")) {
      try {
        generateJS(request, response);
      }
      catch (SecurityException e) {
        throw new ServletException(e);
      }
      catch (IOException e) {
        throw new ServletException(e);
      }
      catch (MarshallException e) {
        throw new ServletException(e);
      }
    } else {
      try {
        String json = readJson(request);
        logger.info(json);
        RemoteInvocation ri = (RemoteInvocation) serializer.fromJSON(json);
        Object obj = buildRemoteInvocation(ri);
        try {
          obj = ri.invoke(this.getService());
        }
        catch (NoSuchMethodException e) {
          throw new ServletException(e);
        }
        OutputStream os = response.getOutputStream();
        String jsonResponse = serializer.toJSON(obj);
        logger.info(jsonResponse);
        os.write(jsonResponse.getBytes());
        response.setContentType("application/json");
        response.setHeader("X-JSON", jsonResponse);
      }

      catch (MarshallException e) {
        throw new ServletException(e);
      }
      catch (UnmarshallException e) {
        throw new ServletException(e);
      }
      catch (IllegalAccessException e) {
        throw new ServletException(e);
      }
      catch (InvocationTargetException e) {
        throw new ServletException(e);
      }
    }
  }

  private void generateJS(HttpServletRequest request, HttpServletResponse response) throws IOException,
      MarshallException {

    Writer writer = response.getWriter();
    writer.write("var Class = {\n" + "  create: function() {\n" + "    return function() {\n"
        + "    this.initialize.apply(this, arguments);\n" + "    }\n" + "  }\n" + "};\n");

    writer.write("Object.extend = function(destination, source) {\n" + "  for (var property in source) {\n"
        + "    destination[property] = source[property];\n" + "  }\n" + "  return destination;\n" + "};\n");
    writer.write("var AjaxInterface = Class.create();" + "AjaxInterface.prototype =\n" + "{\n"
        + "  initialize: function(url,options)\n" + "  {\n" + "    this.options = options;\n" + "    this.url = url;\n"
        + "  }\n" + "};\n" + "\n");

    writer.write("var RemoteInvocation = Class.create();\n" + "RemoteInvocation.prototype =\n" + "{\n"
        + "  initialize: function(methodName, parameterTypes, arguments, attributes)\n" + "  {\n"
        + "    this.methodName = methodName;\n" + "    this.parameterTypes = parameterTypes;\n"
        + "    this.arguments = arguments;\n" + "    this.attributes = attributes;\n" + "  } ,\n"
        + "  javaClass : \"org.springframework.remoting.support.RemoteInvocation\"\n" + "};\n" + "\n");
    String serviceName = beanName.substring(1, beanName.length());
    writer.write(serviceName + " = Class.create();\n");
    writer.write(serviceName + ".prototype =\n");
    writer.write("{\n");
    writer.write("  initialize: function(){\n");
    writer.write("  },\n");

    Method[] methods = this.getServiceInterface().getMethods();
    Set classesToDescribe = new HashSet();
    for (int i = 0; i < methods.length; i++) {
      String methodName = methods[i].getName();
      Class[] parameterTypes = methods[i].getParameterTypes();
      for (int j = 0; j < parameterTypes.length; j++) {
        classesToDescribe.add(parameterTypes[j]);
      }
      writer.write( methodName + " : function(" + buildParameterTypes(parameterTypes)
          + (parameterTypes.length>0?", ":"" )+ "options)\n");
      writer.write("{\n");
      writer.write("  var parameterTypes = new Array();\n");
      writer.write("  var arguments = new Array();\n");

      for (int j = 0; j < parameterTypes.length; j++) {
        writer.write("  parameterTypes[" + j + "] = \"" + parameterTypes[j].getName() + "\"\n");
        writer.write("  arguments[" + j + "] = arg" + j + ";\n");
      }
      writer.write("  var attributes = null;\n");
      writer.write("  var ri = new RemoteInvocation('" + methodName + "', parameterTypes, arguments, attributes );\n");
      writer.write("  var json = toJSON(ri);\n");
      writer.write("  if (options != null){\n");
      writer.write("    options.handle = options.onSuccess;\n");
      writer.write("    options.method = 'post';\n");
      writer.write("  }");
      writer.write("  if (options==null || options.onSuccess == null){\n");
      writer.write("    options = {asynchronous:false, sync:true};\n");
      writer.write("  }");

      writer.write("  var ajaxCall = new AjaxCall(url, options);\n");
      writer.write("  ajaxCall.setBody(json);\n");
      writer.write("  return ajaxCall.makeCall();\n");
      writer.write("},\n\n");
    }
    writer.write("};\n");
    Iterator i = classesToDescribe.iterator();
    while (i.hasNext()) {
      /*
       * var Account = Class.create(); Account.prototype = { initialize:
       * function(name, id) { this.name = name; this.id = id; }, name : '', id :
       * 0, javaClass : "org.springframework.remoting.jsonrpc.test.Account" };
       */
      Class clazz = (Class) i.next();

      writer.write("\nvar " + clazz.getSimpleName() + " = Class.create();\n");
      writer.write(clazz.getSimpleName() + ".prototype = \n");
      writer.write("{\n");
      writer.write("  initialize: function(");
      Method[] clazzMethods = clazz.getMethods();
      boolean firstParam = true;
      for (int j = 0; j < clazzMethods.length; j++) {
        Method method = clazzMethods[j];
        if (method.getName().startsWith("set")) {
          if (!firstParam) writer.write(",");
          else firstParam = false;
          String name = StringUtils.removeStart(method.getName(), "set");
          name = WordUtils.uncapitalize(name);
          writer.write(name);
        }
      }
      writer.write("){\n");
      for (int j = 0; j < clazzMethods.length; j++) {
        Method method = clazzMethods[j];
        if (method.getName().startsWith("set")) {
          String name = StringUtils.removeStart(method.getName(), "set");
          name = WordUtils.uncapitalize(name);
          writer.write("    this." + name + " = " + name + ";\n");
        }
      }
      writer.write("  },\n");
      for (int j = 0; j < clazzMethods.length; j++) {
        Method method = clazzMethods[j];
        if (method.getName().startsWith("set")) {
          String name = StringUtils.removeStart(method.getName(), "set");
          name = WordUtils.uncapitalize(name);
          writer.write("  " + name + " : null,\n");
        }
      }
      writer.write("  javaClass : '" + clazz.getName() + "'\n};\n");
    }
    ajaxHandler.writeAjaxCall(writer);
    ajaxHandler.writeToJson(writer);
  }

  public String buildParameterTypes(Class[] types) {
    logger.info("buildParameterTypes");
    StringBuffer buf = new StringBuffer();
    for (int i = 0; i < types.length; i++) {
      logger.info(types[i]);
      buf.append("arg" + i);
      if (i != types.length - 1) {
        buf.append(", ");
      }
    }
    return buf.toString();
  }

  private String readJson(HttpServletRequest request) throws IOException {
    InputStream is = request.getInputStream();
    byte[] buff = new byte[1024];
    int len = 0;
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    while ((len = is.read(buff)) > 0) {
      out.write(buff, 0, len);
    }
    String json = new String(out.toByteArray());
    return json;
  }

  private RemoteInvocation buildRemoteInvocation(RemoteInvocation ri) throws UnmarshallException, ServletException {
    Object[] arguments = new Object[ri.getArguments().length];
    try {
      for (int i = 0; i < arguments.length; i++) {
        Object riArgument = ri.getArguments()[i];
        Class riParameter = ri.getParameterTypes()[i];
        if (!riArgument.getClass().equals(riParameter)) {
          Object json = null;
          if (riParameter.isPrimitive()) {
            ri.getArguments()[i] = serializer.unmarshall(new SerializerState(), riParameter, riArgument.toString());
          } else if (!riParameter.isInterface() && !riParameter.isArray()) {
            json = new JSONObject(riArgument.toString());
            ri.getArguments()[i] = serializer.unmarshall(new SerializerState(), riParameter, json);
          }

          else {
            ri.getArguments()[i] = serializer.fromJSON(riArgument.toString());
          }

        }
      }
    }
    catch (JSONException e) {
      throw new ServletException(e);
    }

    return ri;
  }

  public void setBeanName(String beanName) {
    this.beanName = beanName;

  }

  public AJAXHandler getAjaxHandler() {
    return ajaxHandler;
  }

  public void setAjaxHandler(AJAXHandler ajaxHandler) {
    this.ajaxHandler = ajaxHandler;
  }

}
